{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> これは[キーボード #1 Advent Calendar 2022](https://adventar.org/calendars/7529)の20日目の記事の一部です。   \n",
    "> トップページは[Pythonだけでキーボードを作る](https://5z6p.com/2022/12/21/ac2022/)です。"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# ケースを作る\n",
    "\n",
    "![目標のケース](../imgs/exported_case.png)\n",
    "\n",
    "cadqueryを使って3Dモデリングし、3Dプリントできるケースを作ります。\n",
    "\n",
    "google colaboratoryではOSの都合上cadqueryのビューアーが使えないため、binderでノートブックを開いています。   \n",
    "binderはDockerを使ってOSや必要なライブラリを予め構成してjupyter notebookが使えます。   \n",
    "そのため、ライブラリなどのインストールは必要ありません。"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# cadqueryとは\n",
    "[cadquery](https://github.com/CadQuery/cadquery)はpythonのコードを書いて3Dモデルを生成する環境です。\n",
    "ビューアーには[jupyter_cadquery](https://github.com/bernhard-42/jupyter-cadquery)を使用します。\n",
    "\n",
    "- cadquery https://github.com/CadQuery/cadquery\n",
    "- cadqueryのドキュメント https://cadquery-ja.readthedocs.io/ja/latest/\n",
    "- jupyter-cadquery https://github.com/bernhard-42/jupyter-cadquery\n",
    "\n",
    "環境を用意します。下のセルを実行してバージョン情報が表示されるビューアーが開くことを確認してください。\n",
    "\n",
    "セルを実行するにはセルをクリックして`Shift + Enter`か上部メニューの`▶`をクリックしてください"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import cadquery as cq\n",
    "\n",
    "from jupyter_cadquery import (\n",
    "    versions,\n",
    "    show, PartGroup, Part, \n",
    "    get_viewer, close_viewer, get_viewers, close_viewers, open_viewer, set_defaults, get_defaults, open_viewer,\n",
    "    get_pick,\n",
    ")\n",
    "\n",
    "from jupyter_cadquery.replay import replay, enable_replay, disable_replay\n",
    "\n",
    "enable_replay(False)\n",
    "\n",
    "set_defaults(\n",
    "    cad_width=640, \n",
    "    height=480, \n",
    ")\n",
    "\n",
    "print()\n",
    "versions()\n",
    "\n",
    "cv = open_viewer(\"CadQuery\", anchor=\"right\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## cadqueryを試してみる\n",
    "簡単な直方体を生成してビューアーに表示します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# xy平面上から直方体を作ってエッジにフィレットをかける\n",
    "box = cq.Workplane('XY').box(1, 2, 3).edges().fillet(0.1)\n",
    "# デフォルトのビューアーに表示する\n",
    "show(box)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# ケースを設計する\n",
    "基板をネジで固定するトレイ型のケースを設計します。\n",
    "\n",
    "## おおまかな外形\n",
    "基板の形を元におおまかな外形を作ります。例に漏れず、定数を宣言して使いまわします。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 基板外形\n",
    "PCB_WIDTH = 76.0\n",
    "PCB_HEIGHT = 57.0\n",
    "PCB_THICKNESS = 1.6\n",
    "\n",
    "# ケースと基板のZ方向のマージン\n",
    "CASE_MARGIN_TOP = 11.0\n",
    "CASE_MARGIN_BOTTOM = 3.5\n",
    "\n",
    "# ケースと基板のXY方向のマージン\n",
    "CASE_MARGIN_PCB = 0.5\n",
    "\n",
    "# ケースの厚み\n",
    "CASE_FRAME = 2.0\n",
    "CASE_BOTTOM = 3.0\n",
    "\n",
    "INNER_HEIGHT = CASE_MARGIN_TOP + PCB_THICKNESS + CASE_MARGIN_BOTTOM\n",
    "CASE_HEIGHT = INNER_HEIGHT + CASE_BOTTOM\n",
    "\n",
    "# XY平面を基準に四角形を書いて押し出す\n",
    "case = (\n",
    "    cq.Workplane(\"XY\")\n",
    "    .rect(\n",
    "        PCB_WIDTH + (CASE_FRAME + CASE_MARGIN_PCB) * 2,\n",
    "        PCB_HEIGHT + (CASE_FRAME + CASE_MARGIN_PCB) * 2,\n",
    "    )\n",
    "    .extrude(CASE_BOTTOM + INNER_HEIGHT)\n",
    "    .edges(\"|Z\")\n",
    "    .fillet(2)\n",
    "    .edges(\"|X\")\n",
    "    .chamfer(1)\n",
    ")\n",
    "\n",
    "show(case)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ケースの内側を切り取る\n",
    "ケースの内側を切り取ってトレイの形にします。   \n",
    "基準の平面を現在のcaseの面から選択してはじめます"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# caseの面からZ軸で一番上にあるものを基準とする\n",
    "# 四角形を書いて内側の高さ分切り取る\n",
    "case = (\n",
    "    case.faces(\">Z\")\n",
    "    .workplane()\n",
    "    .rect(PCB_WIDTH + CASE_MARGIN_PCB * 2, PCB_HEIGHT + CASE_MARGIN_PCB * 2)\n",
    "    .cutBlind(-INNER_HEIGHT)\n",
    ")\n",
    "\n",
    "show(case)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ネジのボスと穴\n",
    "ネジでとめるためのボスと穴を作ります。    \n",
    "`tag(\"name\")`で基準面を保存して使いまわしています。   \n",
    "あらかじめネジ位置の座標の配列を用意して全ての箇所で同じ処理を繰り返します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ネジ位置の座標\n",
    "SCREW_POINTS = [(19, 9.5), (19, -9.5), (-19, -9.5), (0, 9.5)]\n",
    "\n",
    "# caseの面からz軸で一番上にある面(ぎりぎり残ったケースの縁部分)を選択して\n",
    "# INNER_HEIGHT分、下にオフセットした面を基準とする\n",
    "# 各座標に半径2.5mmの円柱を作る\n",
    "# 各座標に半径1.1mmの穴を開ける\n",
    "# 基準面から1mm下にオフセットした面を新たな基準とし、四角い穴を底面まで開ける (ナットを入れる穴)\n",
    "case = (\n",
    "    case.faces(\">Z\")\n",
    "    .workplane(offset=-INNER_HEIGHT)\n",
    "    .tag(\"InnerBottom\")\n",
    "    .pushPoints(SCREW_POINTS)\n",
    "    .circle(2.5)\n",
    "    .extrude(CASE_MARGIN_BOTTOM)\n",
    "    .workplaneFromTagged(\"InnerBottom\")\n",
    "    .pushPoints(SCREW_POINTS)\n",
    "    .circle(1.1)\n",
    "    .cutThruAll()\n",
    "    .workplaneFromTagged(\"InnerBottom\")\n",
    "    .workplane(offset=-1)\n",
    "    .pushPoints(SCREW_POINTS)\n",
    "    .rect(4.2, 4.8)\n",
    "    .cutBlind(-2)\n",
    ")\n",
    "\n",
    "show(case)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## USBコネクタ周り\n",
    "USBコネクタの入る穴を開けます。   \n",
    "USBコネクタは背面に開ける必要があるので基準にする平面はY軸上で一番遠い面を基準にします。   \n",
    "前後で同じ距離の面がありますが、選択したところ背面になったのでそのままにしています。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# USBコネクタの穴位置・寸法\n",
    "USB_POS = (11.25, 1.2 + 3.15 / 2)\n",
    "USB_HOLE_SIZE = [9, 3.2, 7.5]\n",
    "USB_HOLE_MARGIN = 0.5\n",
    "USB_CONN_SIZE = (11, 8)\n",
    "\n",
    "# Y軸方向に一番遠い面を基準にする\n",
    "# USBコネクタの穴は内側まで深めに切り取ってUSBコネクタが当たらないようにする\n",
    "# USBケーブルのハウジングを避ける部分を切り取る\n",
    "case = (\n",
    "    case.faces(\">Y\")\n",
    "    .workplane(centerOption=\"CenterOfMass\")\n",
    "    .center(\n",
    "        PCB_WIDTH / 2 - USB_POS[0],\n",
    "        CASE_HEIGHT / 2 - CASE_MARGIN_TOP - PCB_THICKNESS - USB_POS[1],\n",
    "    )\n",
    "    .tag(\"USBCutout\")\n",
    "    .rect(USB_HOLE_SIZE[0] + USB_HOLE_MARGIN, USB_HOLE_SIZE[1] + USB_HOLE_MARGIN)\n",
    "    .cutBlind(-(CASE_FRAME + USB_HOLE_SIZE[2] + 2))\n",
    "    .workplaneFromTagged(\"USBCutout\")\n",
    "    .rect(USB_CONN_SIZE[0], USB_CONN_SIZE[1])\n",
    "    .cutBlind(-1)\n",
    ")\n",
    "\n",
    "show(case)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## OLEDの下のカバー\n",
    "基板とOLEDの間にはさむカバーを設計します。   \n",
    "別のオブジェクトとして作りますが、位置はあわせます。\n",
    "カバーは両面テープで固定するので特にネジなどは用意しません。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "OLED_COVER_WIDTH = 19\n",
    "OLED_COVER_HEIGHT = 19 * 2 - 3\n",
    "OLED_COVER_THICKNESS = 10\n",
    "\n",
    "# XY平面から基板の上にのる高さ分オフセットした面を基準にする\n",
    "oled_cover = (\n",
    "    cq.Workplane(\"XY\")\n",
    "    .workplane(offset=CASE_BOTTOM + CASE_MARGIN_BOTTOM + PCB_THICKNESS)\n",
    "    .center(-PCB_WIDTH / 2, PCB_HEIGHT / 2)\n",
    "    .tag(\"PCB_ORIGIN\")\n",
    "    .center(OLED_COVER_WIDTH / 2, -OLED_COVER_HEIGHT / 2)\n",
    "    .rect(OLED_COVER_WIDTH, OLED_COVER_HEIGHT)\n",
    "    .extrude(OLED_COVER_THICKNESS)\n",
    "    .faces(\"Z\")\n",
    "    .tag(\"CASE_TOP\")\n",
    "    .edges(\">Y or <X\")\n",
    "    .chamfer(1)\n",
    ")\n",
    "\n",
    "show(case, oled_cover)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3Dデータを出力する\n",
    "最後に3Dプリントできるデータ形式で3Dモデルを出力します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cq.exporters.export(case, \"case.stl\")\n",
    "cq.exporters.export(oled_cover, \"oled_cover.stl\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 3Dプリントする\n",
    "\n",
    "生成したstlファイルを3Dプリントします。\n",
    "\n",
    "![slicer](../imgs/slicer.png)\n",
    "\n",
    "![基板とケース](../imgs/case.jpg)\n",
    "\n",
    "![コネクタ](../imgs/case_usb.jpg)\n",
    "\n",
    "印刷したあと基板をのせて確認しました。\n",
    "コネクタ位置も完璧です。\n",
    "\n",
    "これでケースは完成です！"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]"
  },
  "vscode": {
   "interpreter": {
    "hash": "b68c839e49670d3aa77ea8bdd6f541fdcce81585b85d6e1d97674ad5f32db92f"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
